# 《WaLiOffice - AI Agent 智能办公平台》第3-3节:联网搜索工具 web_search
作者:小傅哥
博客:https://bugstack.cn (opens new window)
沉淀、分享、成长,让自己和他人都能有所收获!😄
大家好,我是技术UP主小傅哥。
前两节的 md_generate 和 doc_generate 都是"纯生成"类工具——LLM 根据用户需求凭空创造内容。但真实办公场景里经常出现另一类需求:查竞品动态、查政策原文、查某个库的最新版本号。这些信息在 LLM 的训练数据里不存在或者过时,编是编不出来的。
这节我们来写第一个不依赖 LLM 生成内容的工具——web_search。它也是全项目工具里唯一一个 is_read_only = true 的工具:只读外部世界,不产生新内容,产物是一张"搜索结果卡片"。
但联网搜索绝不是"调一个搜索 API"就完事。真实工程里要解决四个问题:
- 多 Provider:中文搜索和英文搜索的最优数据源不同,需要一个 Provider 抽象
- 自动降级:搜索服务是最不稳定的外部依赖(反爬、限流、Key 过期),主源挂了要无缝切换
- 协议适配:百度 MCP 走 SSE + JSON-RPC,SearXNG 返回 JSON,百度移动版/DuckDuckGo 返回 HTML——三种协议要统一成一个返回结构
- 链路可观测:降级发生后,用户应该能看到"这次到底用了哪个源"
# 一、本章诉求
- 理解 SearchProvider 抽象:四种 Provider 的数据源、协议和适用场景
- 掌握 auto 模式的语言路由:
contains_cjk判断中英文,动态组装降级链,未配置 Key 的 Provider 自动剔除 - 实现百度 MCP 客户端:SSE 长连接 + JSON-RPC 握手(initialize → notifications/initialized → tools/call),流式等待结果
- 掌握三种结果解析:JSON 直接反序列化(SearXNG)、HTML 正则提取(百度/DuckDuckGo)、纯文本正则提取(百度 MCP)
- 理解结果清洗链:HTML 标签清理、HTML 实体转义、URL 归一化(DuckDuckGo 的
uddg重定向参数)、snippet 截断 - 掌握产物与状态设计:
kind: "search"产物、两次state_update(搜索中 → 检索来源)、providers_tried链路透出
# 二、流程设计
# 2.1 web_search 调用链路

ReAct 循环:LLM 决策调用 web_search(query, max_results)
↓
① 参数提取:query trim、max_results clamp(1, 10)
query 为空 → ToolResult::err("query 不能为空")
↓
② ctx.send("state_update"):正在搜索:{query}
↓
③ search_web(query, max_results)
├─ 读配置 AIPPT_WEB_SEARCH_PROVIDER(默认 auto)
├─ 构建共享 reqwest Client(超时 AIPPT_WEB_SEARCH_TIMEOUT_MS,默认 20s)
├─ 指定 provider → 直接调用,失败即失败
└─ auto → contains_cjk(query) 判断语言
↓ 中:[baidu_mcp(有Key才加), baidu, searxng, duckduckgo]
↓ 英:[searxng, duckduckgo, baidu_mcp(有Key才加), baidu]
↓ 依次尝试,第一个返回非空结果的 Provider 胜出
↓
④ ctx.send("state_update"):本次使用 {provider_label},检索链路:xxx -> yyy
↓
⑤ 构建 ToolArtifact { kind: "search" }
content = { type, query, provider, provider_label, providers_tried, results }
↓
⑥ ToolResult
├─ observation:结果列表编号文本(给 LLM 供后续引用)
└─ data:同产物 JSON(给 Agent/前端参考)
↓
前端 SearchArtifact 渲染:查询词、来源、结果数量、检索链路标签、结果卡片列表
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

